package ade.prime.service;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import ade.prime.activity.AdeWebNotification;
import ade.prime.activity.R;
import ade.prime.activity.adapter.ResourceFetcher;
import ade.prime.analyzer.ContainerData;
import ade.prime.analyzer.FeedAnalyzer;
import ade.prime.analyzer.FeedConnection;
import ade.prime.analyzer.FeedLesson;
import ade.prime.database.Database;
import ade.prime.exception.AAAException;
import ade.prime.exception.NoConnectionException;
import ade.prime.model.AdeWebOptions;
import ade.prime.model.GuidScheduleUpdater;
import ade.prime.model.IntentConstants;
import ade.prime.model.Lesson;
import ade.prime.model.LessonType;
import ade.prime.model.Lessons;
import ade.prime.model.Project;
import ade.prime.model.Resource;
import ade.prime.model.Schedule;
import ade.prime.model.ScheduleState;
import ade.prime.model.ScheduleUpdater;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Pair;

/**
 * @author		Schnell Michaël
 * @version		1.0
 */
public class AdeWebService extends Service
implements OnSharedPreferenceChangeListener, IntentConstants {

	/**
	 * Used for logging purposes.
	 */
	protected static final String TAG = AdeWebService.class.getSimpleName() ;
	
//	private static final String AUTO_UPDATE_TIMER_NAME = 
//			"AutoUpdateTimer" ; //$NON-NLS-1$
//
//	private static final long INITIAL_TIMER_DELAY = 1000L ;
//	
//	private static final long N_MILLIS_PER_HOURS = 60L * 60L * 1000L ;

	private static final long INITIAL_UPDATE_DELAY = 1000L ;
	
	private static final int DEPRECATED_LIMIT_IN_DAYS = 3 ;

	/**
	 * Object responsible for periodically calling the update 
	 * of the schedules.
	 */
	protected Timer timer ;

	/**
	 * Callback doing the actual schedule update.
	 */
	protected TimerTask autoUpdateTask ;

	/**
	 * List of the listeners alerted when a schedule update occurred.
	 */
	protected List<AdeWebServiceListener> listeners ;

	/**
	 * API end point for this service.
	 */
	protected ApiEndPoint apiEndpoint ;
	
	/**
	 * Queue containing the schedules waiting for an update.
	 */
	protected List<Pair<ScheduleState, Schedule>> updateQueue ;
	
	/**
	 * Runnable used in a separate thread for updating the schedules in the
	 * update queue.
	 */
	protected ScheduledExecutorService scheduler ;

	/**
	 * {@inheritDoc}
	 */
	@Override
	public IBinder onBind(Intent intent) {
		if (AdeWebService.class.getName().equals(intent.getAction())) {
//			Log.d(TAG, "Bound by intent " + intent) ; //$NON-NLS-1$
			return this.apiEndpoint ;
		}// if
		return null ;
	}// onBind(Intent)

	@Override
	public void onCreate() {
		super.onCreate() ;
//		Log.i(TAG, "Service creating"); //$NON-NLS-1$
		this.listeners = new ArrayList<AdeWebServiceListener>() ;
		this.updateQueue = new ArrayList<Pair<ScheduleState, Schedule>>() ;
		this.apiEndpoint = new ApiEndPoint() ;
		this.scheduler = Executors.newScheduledThreadPool(1) ;

		if (AdeWebOptions.autoUpdateIsOn(this)) {
			this.startAutoUpdates() ;
		}// if
		this.startScheduleStateManager() ;
		
		// Add listener for preference changes
		AdeWebOptions.registerChangeListener(this, this) ;
	}// onCreate()
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (intent != null) {
			String action = intent.getAction() ;
//			Log.d(TAG, String.format("[onStartCommand] action: '%s'", //$NON-NLS-1$
//					action)) ;
			if (action.equals(SCHEDULE_STATE_UPDATE_ACTION)) {
				this.updateDeprecatedScheduleStates() ;
			} else if (action.equals(SCHEDULE_AUTO_UPDATE_ACTION)) {
				this.launchAutoUpdate() ;
			} else if (action.equals(REPLICATE_PREFERENCES_ACTION)) {
				this.replicatePreferences(intent.getExtras()) ;
			}// if
		}// if
		return super.onStartCommand(intent, flags, startId) ;
	}// onStartCommand(Intent, int, int)

	@Override
	public void onDestroy() {
		super.onDestroy() ;
//		Log.i(TAG, "Service destroying") ; //$NON-NLS-1$
		
		// Remove listener for preference changes
		AdeWebOptions.unregisterChangeListener(this, this) ;

		if (this.timer != null) {
			this.cancelAutoUpdates() ;
		}// if
	}// onDestroy()
	
	protected void startAutoUpdates() {
//		this.timer = new Timer(AUTO_UPDATE_TIMER_NAME) ;
//		this.autoUpdateTask = new AutoUpdateTask() ;
//		int nHours = AdeWebOptions.getnHoursBetweenUpdates(this) ;
//		long updateDelay = nHours * N_MILLIS_PER_HOURS ;
//		this.timer.schedule(this.autoUpdateTask, INITIAL_TIMER_DELAY, updateDelay) ;
		long nHours = AdeWebOptions.getnHoursBetweenUpdates(this) ;
		AlarmManager am = (AlarmManager) this.getSystemService(ALARM_SERVICE) ;
		Intent intent = new Intent(SCHEDULE_AUTO_UPDATE_ACTION) ;
		PendingIntent autoUpdateBroadcast = 
				PendingIntent.getBroadcast(this, 0, intent, 0) ;
		am.setRepeating(AlarmManager.RTC, 
				System.currentTimeMillis() + 10L, 
				nHours * 60L * 60L * 1000L,
				autoUpdateBroadcast) ;
	}// startAutoUpdates()
	
	protected void cancelAutoUpdates() {
//		this.timer.cancel() ;
//		this.timer = null ;
//		this.autoUpdateTask = null ;
		AlarmManager am = (AlarmManager) this.getSystemService(ALARM_SERVICE) ;
		Intent intent = new Intent(SCHEDULE_AUTO_UPDATE_ACTION) ;
		PendingIntent autoUpdateBroadcast = 
				PendingIntent.getBroadcast(this, 0, intent, 0) ;
		am.cancel(autoUpdateBroadcast) ;
	}// cancelAutoUpdates()
	
	private void startScheduleStateManager() {
		// Start the "schedule status manager"
		AlarmManager am = (AlarmManager) this.getSystemService(ALARM_SERVICE) ;
		Intent intent = new Intent(SCHEDULE_STATE_UPDATE_ACTION) ;
		PendingIntent scheduleStateBroadCast = 
				PendingIntent.getBroadcast(this, 0, intent, 0) ;
		am.setRepeating(AlarmManager.RTC, 
				System.currentTimeMillis() + 10L, 
				24L * 60L * 60L * 1000L,
				scheduleStateBroadCast) ;
	}// startScheduleStateManager()

//	/**
//	 * Callback for the timer doing the actual schedule update.
//	 * 
//	 * @author		Schnell Michaël
//	 * @version		1.0
//	 */
//	private class AutoUpdateTask extends TimerTask {
//
//		/**
//		 * Sole constructor.
//		 */
//		protected AutoUpdateTask() {
//			super() ;
//		}// SchedulUpdate()
//
//		/**
//		 * Called by the timer, updates the schedules.
//		 */
//		@Override
//		public void run() {
//			AdeWebService service = AdeWebService.this ;
//			synchronized (service.updateQueue) {
//				try {
//					for(Schedule schedule : service.apiEndpoint.getAllSchedules()) {
//						service.apiEndpoint.updateSchedule(schedule) ;
//					}// for
//				} catch (RemoteException re) {
//					Log.d(AdeWebService.TAG, 
//							"Failed to auto-update all schedules.", re) ; //$NON-NLS-1$
//				}// try
//			}// synchronized
//		}// run
//
//	}// class AutoUpdateTask

	private class ApiEndPoint extends AdeWebServiceApi.Stub {
		
		protected int nextListenerId ;
		
		private AdeWebService service ;
		
		protected final Object databaseWriteLock ;
		
		/**
		 * 
		 */
		public ApiEndPoint() {
			super() ;
			this.service = AdeWebService.this ;
			this.databaseWriteLock = new Object() ;
			this.nextListenerId = 0 ;
		}// ApiEndPoint()

		@Override
		public int addListener(AdeWebServiceListener listener)
				throws RemoteException {
			synchronized (this.service.listeners) {
				this.service.listeners.add(listener) ;
			}// synchronized
			return this.nextListenerId++ ;
		}// addListener(AdeWebServiceListener)

		@Override
		public void removeListener(int listenerId)
				throws RemoteException {
			synchronized (this.service.listeners) {
				Iterator<AdeWebServiceListener> iter = 
						this.service.listeners.iterator() ;
				boolean removed = false ;
				while (iter.hasNext() && !removed) {
					AdeWebServiceListener next = iter.next() ;
					if (next.getId() == listenerId) {
						iter.remove() ;
						removed = true ;
					}// if
				}// while
			}// synchronized
		}// removeListener(AdeWebServiceListener)

		@Override
		public Schedule createSchedule(String name, Resource[] resources,
				int period, Project project) throws RemoteException {
//			Log.d(TAG, "Service - Creating schedule, values:") ; //$NON-NLS-1$
//			Log.d(TAG, "-- name: '" + name + "'") ; //$NON-NLS-1$ //$NON-NLS-2$
//			Log.d(TAG, "-- resources: " + Arrays.toString(resources)) ; //$NON-NLS-1$
//			Log.d(TAG, "-- period: " + period) ; //$NON-NLS-1$
//			Log.d(TAG, "-- project: " + project) ; //$NON-NLS-1$
			Schedule schedule = new Schedule(name, resources, period, project) ;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				try {
					schedule.validate(db) ;
					db.insertSchedule(schedule) ;
				} catch (AAAException ae) {
					Log.d(TAG, "-- Failed validate.", ae) ; //$NON-NLS-1$
					schedule = null ;
				}// try
			}// synchronized
//			Log.d(TAG, "-- Schedule -" + schedule) ; //$NON-NLS-1$
			if (schedule != null) {
				synchronized (this.service.listeners) {
					for(AdeWebServiceListener listener : this.service.listeners) {
						listener.handleNewSchedule(schedule) ;
					}// for
				}// synchronized
			}// if
			return schedule ;
		}// createSchedule(String, Resource[],int, Project)

		@Override
		public Schedule editSchedule(Schedule oldSchedule, String name,
				Resource[] resources, int period, Project project)
				throws RemoteException {
//			Log.d(TAG, "Service - Modifying schedule, values:") ; //$NON-NLS-1$
//			Log.d(TAG, "-- name: '" + name + "'") ; //$NON-NLS-1$ //$NON-NLS-2$
//			Log.d(TAG, "-- resources: " + Arrays.toString(resources)) ; //$NON-NLS-1$
//			Log.d(TAG, "-- period: " + period) ; //$NON-NLS-1$
//			Log.d(TAG, "-- project: " + project) ; //$NON-NLS-1$
			Schedule schedule = new Schedule(oldSchedule) ;
			schedule.setName(name) ;
			schedule.setResources(resources) ;
			schedule.setPeriod(period) ;
			schedule.setProject(project) ;
			if (!oldSchedule.equals(schedule)) {
				String oldName = oldSchedule.getName() ;
				synchronized (this.databaseWriteLock) {
					Database db = new Database(this.service) ;
					try {
						if (oldName.equals(name)) {
							schedule.validateOnUpdate() ;
						} else {
							schedule.validate(db) ;
						}// if
						db.updateSchedule(oldSchedule, schedule) ;
					} catch (AAAException ae) {
						Log.d(TAG, "-- Failed validate.", ae) ; //$NON-NLS-1$
						schedule = null ;
					}// try
				}// synchronized
//				Log.d(TAG, "-- Schedule -" + schedule) ; //$NON-NLS-1$
				if (schedule != null) {
					synchronized (this.service.listeners) {
						for(AdeWebServiceListener listener : this.service.listeners) {
							listener.handleModifiedSchedule(schedule) ;
						}// for
					}// synchronized
				}// if
			}// if
			return schedule ;
		}// editSchedule(Schedule, String, Resource[], int, Project)
		
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void updateScheduleState(int scheduleId,
				ScheduleState newState) throws RemoteException {
			Schedule schedule = this.getSchedule(scheduleId) ;
			this.updateScheduleState(schedule, newState) ;
		}// updateScheduleState(int, ScheduleState)
		
		@Override
		public Schedule updateScheduleNotify(Schedule schedule)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				db.updateSchedule(schedule.getId(), schedule) ;
			}// synchronized
			return schedule ;
		}// editSchedule(Schedule)

		@Override
		public void removeSchedule(Schedule schedule) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				db.removeSchedule(schedule.getId()) ;
			}// synchronized
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					listener.handleRemovedSchedule(schedule) ;
				}// for
			}// synchronized
		}// removeSchedule(Schedule)

		@Override
		public void updateSchedule(Schedule schedule) throws RemoteException {
			this.updateSchedule(schedule, ScheduleState.NOT_UPDATED) ;
		}// updateSchedule(Schedule)

		protected void updateSchedule(Schedule schedule, ScheduleState failureState) {
			if (AdeWebOptions.networkIsAvailable(this.service)) {
				Schedule addedSchedule = null ;
				synchronized (this.service.updateQueue) {
					if (!this.service.updateQueue.contains(schedule)) {
						addedSchedule = new Schedule(schedule) ;
						Pair<ScheduleState, Schedule> scheduleWithState = 
								new Pair<ScheduleState, Schedule>(
										failureState, addedSchedule) ;
						this.service.updateQueue.add(scheduleWithState) ;
						this.service.startScheduleUpdateTask() ;
					}// if
				}// synchronized
				if (addedSchedule != null) {
					this.updateScheduleState(addedSchedule, 
							ScheduleState.WAITING_FOR_UPDATE) ;
				}// if
			}// if
		}// updateSchedule(Schedule)
		
		protected void updateScheduleState(Schedule schedule, ScheduleState newState) {
//			Log.d(TAG, String.format("[updateScheduleState]: %s (%s) - %s.%s",
//					schedule.getName(), Integer.valueOf(schedule.getId()),
//					newState, newUpdateState)) ;
			if (!schedule.getState().equals(newState)) {
				schedule.setState(newState) ;
				synchronized (this.databaseWriteLock) {
					Database db = new Database(this.service) ;
					db.updateScheduleState(schedule.getId(), newState) ;
				}// synchronized
			}// if
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					try {
						listener.handleModifiedScheduleState(schedule) ;
					} catch (RemoteException e) {
						// TODO Handle
						Log.d(TAG, "Failed to notify of schedule state change", e) ; //$NON-NLS-1$
					}// try
				}// for
			}// synchronized
		}// updateScheduleState(Schedule, ScheduleState)

		@Override
		public void cancelScheduleUpdate(Schedule schedule)
				throws RemoteException {
			boolean stateChange = false ;
			if (!schedule.getState().equals(ScheduleState.UPDATING)) {
				synchronized (this.service.updateQueue) {
					if (this.service.updateQueue.contains(schedule)) {
						this.service.updateQueue.remove(schedule) ;
						stateChange = true ;
					}// if
				}// synchronized
				if (stateChange) {
					this.updateScheduleState(schedule, ScheduleState.NOT_UPDATED) ;
				}// if
			}// if
		}// cancelScheduleUpdate(Schedule)

		@Override
		public List<Schedule> getAllSchedules() throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getAllSchedules() ;
			}// synchronized
		}// getAllSchedules()

		@Override
		public ScheduleState getScheduleState(Schedule schedule)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getSchedule(schedule.getId()).getState() ;
			}// synchronized
		}// getScheduleState(Schedule)

		@Override
		public Lesson getLesson(long lessonId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getLesson(lessonId) ;
			}// synchronized
		}// getLesson(long)

		@Override
		public LessonType getLessonType(Lesson lesson) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getLesson(lesson.getLessonId()).getType() ;
			}// synchronized
		}// getLessonType(Lesson)

		@Override
		public Lesson setLessonType(Lesson lesson, LessonType type)
				throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				lesson.setType(type) ;
				db.updateLesson(lesson.getLessonId(), lesson) ;
			}// synchronized
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					listener.handleModifiedLessonType(lesson) ;
				}// for
			}// synchronized			
			return lesson ;
		}// setLessonType(Lesson lessen, LessonType)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Schedule getSchedule(int scheduleId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				return db.getSchedule(scheduleId) ;
			}// synchronized
		}// getSchedule(int)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Lessons getAllScheduleLessons(int scheduleId)
				throws RemoteException {
			Lesson[] lessons ;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				lessons = db.getAllScheduleLessons(scheduleId) ;
			}// synchronized
			return new Lessons(lessons) ;
		}// getAllScheduleLessons(String)

		/**
		 * {@inheritDoc}
		 */
		@Override
		public Lessons getFutureScheduleLessons(int scheduleId)
				throws RemoteException {
			Lesson[] lessons ;
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				lessons = db.getFutureScheduleLessons(scheduleId) ;
			}// synchronized
			return new Lessons(lessons) ;
		}// getFutureScheduleLessons(String)
		
		/**
		 * {@inheritDoc}
		 * @throws RemoteException 
		 */
		@Override
		public void removeLessonHistory(long lessonId) throws RemoteException {
			synchronized (this.databaseWriteLock) {
				Database db = new Database(this.service) ;
				db.removeLessonHistory(lessonId) ;
			}// synchronized
			synchronized (this.service.listeners) {
				for(AdeWebServiceListener listener : this.service.listeners) {
					listener.handleRemovedLessonHistory(lessonId) ;
				}// for
			}// synchronized
		}// removeLessonHistory(long)

	}// class ApiEndPoint()
	
	protected void startScheduleUpdateTask() {
		this.scheduler.schedule(new ScheduleUpdateRunnable(), 
				INITIAL_UPDATE_DELAY, TimeUnit.MILLISECONDS) ;
	}// startScheduleUpdateTask()
	
	private class ScheduleUpdateRunnable implements Runnable {
		protected ScheduleUpdateRunnable() {
			super() ;
		}// ScheduleUpdateRunnable()
		/**
		 * {@inheritDoc}
		 */
		@Override
		public void run() {
			AdeWebService.this.updateAllSchedules() ;
		}// run()
	}// class ScheduleUpdateRunnable
	
	protected void updateAllSchedules() {
		AdeWebNotification.initScheduleNotificationNumber() ;
		while (!this.updateQueue.isEmpty()) {
			
			Pair<ScheduleState, Schedule> updateValues ;
			synchronized (this.updateQueue) {
				updateValues = this.updateQueue.get(0) ;
				this.updateQueue.remove(0) ;
			}// synchronized

			Database db = new Database(this) ;
			Schedule schedule = db.getSchedule(updateValues.second.getId()) ;
			db.close() ;
			boolean notify = schedule.isNotify() ;
			this.updateSchedule(schedule, updateValues.first, notify) ;			
		}// while
	}// updateAllSchedules(boolean)
	
	protected void updateSchedule(Schedule schedule, ScheduleState failureState, 
			boolean notify) {
		Database db = new Database(this) ;
		try {
			this.connectToAdeApi(schedule) ;
			this.setProject(schedule) ;
			this.getLessonsFeed(schedule) ;
			this.analyzeLessonsFeed(schedule) ;
			synchronized (this.apiEndpoint.databaseWriteLock) {
				this.compareWithPreviousSchedule(schedule, db) ;
				this.updateScheduleInsideDatabase(schedule, db) ;
			}// synchronized
			ScheduleState.UPDATED.step = ScheduleUpdateProgress.STEP_6 ;
			this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATED) ;
			if (notify) {
				// Check if any lesson planned within the next two days was modified
				if (this.scheduleDiff.importantChange()) {
					AdeWebNotification.makeScheduleNotification(this, schedule) ;
				}// if
			}// if
			this.modifyScheduleLastUpdate(schedule) ;
		} catch (Exception ae) {
			Log.d(TAG, String.format("Failed to update schedule %s (%d)", //$NON-NLS-1$
					schedule.getName(), Integer.valueOf(schedule.getId())), ae) ;
			failureState.step = schedule.getState().step ;
			this.apiEndpoint.updateScheduleState(schedule, failureState) ;
		} finally {
			db.close() ;
		}// try
	}// updateSchedule(Schedule)
	
	protected void modifyScheduleLastUpdate(Schedule schedule) {
		String now = Database.dateFormatter.format(new Date()) ;
		schedule.setLastUpdate(now) ;
		Database db = new Database(this) ;
		db.updateSchedule(schedule.getId(), schedule) ;
		db.close() ;
		for(AdeWebServiceListener listener : this.listeners) {
			try {
				listener.handleModifiedSchedule(schedule) ;
			} catch (RemoteException re) {
				Log.d(TAG, String.format("Failed to notify of change for last update for schedule %s (%d)", //$NON-NLS-1$
						schedule.getName(), Integer.valueOf(schedule.getId())), re) ;
			}// try
		}// for
	}// modifyScheduleLastUpdate(Schedule)
	
	protected FeedConnection connection ;
	
	protected List<FeedLesson> lessonsFeed ;
	
	protected Schedule newSchedule ;
	
	protected ScheduleUpdater scheduleDiff ;
	
	protected void connectToAdeApi(Schedule schedule) throws AAAException {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_1 ;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING) ;

		// Check network availability
		this.checkNetworkAvailability() ;
		
		this.connection = ContainerData.getFeedConnexion() ;
		if (this.connection == null) {
			String errmsg = 
					this.getResources().getString(R.string.ADE_connection_failed) ;
			throw new AAAException(errmsg) ;
		}// if
	}// connectToAdeApi(Schedule)
	
	private void checkNetworkAvailability() throws NoConnectionException {
		ConnectivityManager connMgr = (ConnectivityManager) 
				getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
		if ((networkInfo == null) || !networkInfo.isConnected()) {
			throw new NoConnectionException() ;
		}// if
	}// checkNetWorkAvailability()
	
	protected void setProject(Schedule schedule) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_2 ;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING) ;

		String sessionId = this.connection.getId() ;
//		this.connection = 
		ContainerData.getFeedSetProject(sessionId, 
				schedule.getProject().getAdeProjectId()) ;
	}// setProject(Schedule)
	
	protected void getLessonsFeed(Schedule schedule) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_3 ;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING) ;
		
		String sessionId = this.connection.getId() ;
		schedule.setStudentNumList() ;
		this.lessonsFeed = ContainerData.getFeedsLesson(sessionId, 
				schedule.getAllResources(), schedule.getPeriod()) ;
		// On a fini la récupération de données depuis ADE, on se déconnecte
		this.connection = ContainerData.getFeedDeconnexion(sessionId) ;
	}// getLessonsFeed()
	
	protected void analyzeLessonsFeed(Schedule schedule) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_4 ;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING) ;
		
		this.newSchedule = new Schedule() ;
		this.newSchedule.setId(schedule.getId()) ;
		FeedAnalyzer feedAnalyzer = 
				new FeedAnalyzer(this.newSchedule, this.lessonsFeed) ;
		feedAnalyzer.start() ;
	}// analyzeLessonsFeed()
	
	protected void compareWithPreviousSchedule(Schedule schedule,
			Database db) {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_5 ;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING) ;
		
		schedule.buildComplete(db) ;
		this.scheduleDiff = new GuidScheduleUpdater() ;
		this.scheduleDiff.compare(schedule, this.newSchedule) ;
	}// compareWithPreviousSchedule()
	
	protected void updateScheduleInsideDatabase(Schedule schedule,
			Database db) throws AAAException {
		ScheduleState.UPDATING.step = ScheduleUpdateProgress.STEP_6 ;
		this.apiEndpoint.updateScheduleState(schedule, ScheduleState.UPDATING) ;

		this.scheduleDiff.updateSchedule(db) ;
	}// updateScheduleInsideDatabase()
	
	/**
	 * 
	 */
	private void updateDeprecatedScheduleStates() {
		// Retrieve all schedules and keep the deprecated ones
		Database db = new Database(this) ;
		List<Schedule> deprecated = new ArrayList<Schedule>() ;
		String deprecatedLimt = getDeprecatedLimit() ;
		for(Schedule schedule : db.getAllSchedules()) {
//			Log.d(TAG, String.format("[updateDeprecatedScheduleStates] " + //$NON-NLS-1$
//					"%s(%d) - lastUpdate: '%s' - limit: '%s' ? deprecated: %b", //$NON-NLS-1$
//					schedule.getName(),
//					Integer.valueOf(schedule.getId()),
//					schedule.getLastUpdate(),
//					deprecatedLimt,
//					Boolean.valueOf(schedule.getLastUpdate().compareTo(deprecatedLimt) < 0))) ;
			if (!schedule.getState().equals(ScheduleState.OTHER) 
					&& schedule.getLastUpdate().compareTo(deprecatedLimt) < 0) {
				deprecated.add(schedule) ;
			}// if
		}// for

		// Update the state to OTHER for all deprecated schedules 
		for(Schedule schedule : deprecated) {
			db.updateScheduleState(schedule.getId(), ScheduleState.OTHER) ;
			try {
				this.apiEndpoint.updateScheduleState(schedule.getId(), ScheduleState.OTHER) ;
			} catch (RemoteException re) {
				Log.d(TAG, "Failed to notify of schedule state change.", //$NON-NLS-1$
						re) ;
			}// try
		}// for
	}// updateDeprecatedScheduleStates()
	
	private static String getDeprecatedLimit() {
		Calendar calendar = Calendar.getInstance() ;
		calendar.add(Calendar.DAY_OF_YEAR, -DEPRECATED_LIMIT_IN_DAYS) ;
		Date limit = calendar.getTime() ;
		return Database.dateFormatter.format(limit) ;
	}// getDeprecatedLimit()
	
	protected void replicatePreferences(Bundle preferences) {
		String preferenceKey ;
		SharedPreferences.Editor editor = 
				PreferenceManager.getDefaultSharedPreferences(this).edit() ;
		
		// Auto-update
		preferenceKey = ResourceFetcher.getString(this, R.string.Preferences_sync) ;
		if (preferences.containsKey(preferenceKey)) {
			editor.putBoolean(preferenceKey, preferences.getBoolean(preferenceKey)) ;
		}// if
		// Number of hours between two updates
		preferenceKey = ResourceFetcher.getString(this, R.string.Preferences_sync_delay) ;
		if (preferences.containsKey(preferenceKey)) {
			editor.putString(preferenceKey, preferences.getString(preferenceKey)) ;
		}// if
		// Theme
		preferenceKey = ResourceFetcher.getString(this, R.string.Preferences_theme) ;
		if (preferences.containsKey(preferenceKey)) {
			editor.putString(preferenceKey, preferences.getString(preferenceKey)) ;
		}// if
		
		editor.commit() ;
	}// replicatePreferences(Bundle)

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
			String key) {
//		Log.d(TAG, String.format("[onSharedPreferenceChanged] key: '%s'\n" + //$NON-NLS-1$
//				"\tSync: '%s' =?= %b" + //$NON-NLS-1$
//				"\tnHrs: '%s' =?= %b", //$NON-NLS-1$
//				key, 
//				ResourceFetcher.getString(this, R.string.Preferences_sync),
//				Boolean.valueOf(key.equals(ResourceFetcher.getString(this, R.string.Preferences_sync))), 
//				ResourceFetcher.getString(this, R.string.Preferences_sync_delay),
//				Boolean.valueOf(key.equals(ResourceFetcher.getString(this, R.string.Preferences_sync_delay))))) ; 
		
		if (key.equals(ResourceFetcher.getString(this, R.string.Preferences_sync))) {
			if (AdeWebOptions.autoUpdateIsOn(this)) {
				this.startAutoUpdates() ;
			} else {
				this.cancelAutoUpdates() ;
			}// if
		} else if (key.equals(ResourceFetcher.getString(this, R.string.Preferences_sync_delay))) {
			this.cancelAutoUpdates() ;
			this.startAutoUpdates() ;
		} else if (key.equals(ResourceFetcher.getString(this, R.string.Preferences_theme))) {
//			Log.d(TAG, "New theme, alert listeners") ;
			synchronized (this.listeners) {
				for(AdeWebServiceListener listener : this.listeners) {
					try {
						listener.handleModifiedOptions() ;
					} catch (RemoteException re) {
						Log.d(TAG, "Failed to notify of options change", re) ; //$NON-NLS-1$
					}// try
				}// for
			}// synchronized
		}// if
	}// onSharedPreferenceChanged(SharedPreferences, String)
	
	protected void launchAutoUpdate() {
		synchronized (this.updateQueue) {
			try {
				for(Schedule schedule : this.apiEndpoint.getAllSchedules()) {
					this.apiEndpoint.updateSchedule(schedule, schedule.getState()) ;
				}// for
			} catch (RemoteException re) {
				Log.d(AdeWebService.TAG, 
						"Failed to auto-update all schedules.", re) ; //$NON-NLS-1$
			}// try
		}// synchronized
	}// launchAutoUpdate()

}// class AdeWebService